<template>
    <div class="page">
        <!-- 顶部返回 -->
        <div class="page-header">
            <span class="back" role="button" @click="goBack">返回</span>
            <span class="sep">|</span>
            <span class="title">{{ pageTitle }}</span>
        </div>

        <!-- 表单主体 -->
        <a-form class="form" :model="form" :rules="rules" layout="horizontal" :label-col="{ flex: '140px' }"
            :wrapper-col="{ flex: 'auto' }" :colon="false" ref="formRef">
            <!-- 基础信息 -->
            <a-form-item label="接口名称：" name="name" class="row-40">
                <a-input v-model:value="form.name" placeholder="请输入" />
            </a-form-item>

            <a-form-item label="启用状态：" name="enabled" class="row-40">
                <a-radio-group v-model:value="form.enabled">
                    <a-radio value="enabled">启用</a-radio>
                    <a-radio value="disabled">停用</a-radio>
                </a-radio-group>
            </a-form-item>

            <a-form-item label="开放状态：" name="public" class="row-40">
                <a-radio-group v-model:value="form.public">
                    <a-radio value="public">公开</a-radio>
                    <a-radio value="private">私密</a-radio>
                </a-radio-group>
            </a-form-item>

            <!-- 字典选择 -->
            <a-form-item label="接口类型：" name="apiType" class="row-40">
                <a-select v-model:value="form.apiType" placeholder="请选择" allow-clear :loading="dictLoading.apiType">
                    <a-select-option v-for="opt in apiTypeOptions" :key="opt.value" :value="opt.value">
                        {{ opt.label }}
                    </a-select-option>
                </a-select>
            </a-form-item>

            <a-form-item label="输入类型：" name="inputType" class="row-40">
                <a-select v-model:value="form.inputType" placeholder="请选择" allow-clear :loading="dictLoading.inputType">
                    <a-select-option v-for="opt in inputTypeOptions" :key="opt.value" :value="opt.value">
                        {{ opt.label }}
                    </a-select-option>
                </a-select>
            </a-form-item>

            <a-form-item label="数据类型：" name="dataType" class="row-40">
                <a-select v-model:value="form.dataType" placeholder="请选择" allow-clear :loading="dictLoading.dataType">
                    <a-select-option v-for="opt in dataTypeOptions" :key="opt.value" :value="opt.value">
                        {{ opt.label }}
                    </a-select-option>
                </a-select>
            </a-form-item>

            <!-- 索引关联 -->
            <a-form-item label="索引关联：" name="indexBinding" class="row-40">
                <a-space>
                    <a-button @click="openIndexModal">选择</a-button>
                </a-space>
            </a-form-item>

            <!-- 索引映射表 -->
            <div v-if="indexLinked" class="index-table-card">
                <div class="index-table-head">
                    <span class="name">{{ form.indexBinding || '（已选索引）' }}</span>
                    <a-tooltip title="移除">
                        <CloseOutlined class="close" @click="clearIndexBinding" />
                    </a-tooltip>
                </div>

                <a-table :data-source="mappingRows" :columns="mappingColumns" row-key="key" bordered :pagination="false"
                    class="mapping-table">
                    <template #bodyCell="{ column, record, index }">
                        <template v-if="column.dataIndex === 'searchField'">
                            <div class="cell-stretch">
                                <a-input v-model:value="record.searchField" placeholder="请输入" @blur="onCellBlur" />
                            </div>
                        </template>

                        <template v-else-if="column.dataIndex === 'searchMode'">
                            <div class="cell-stretch">
                                <a-select v-model:value="record.searchMode" placeholder="请选择（单选）"
                                    :loading="searchModeLoading" allow-clear :dropdownMatchSelectWidth="false"
                                    @change="onSearchModeChange" @blur="onCellBlur">
                                    <a-select-option v-for="opt in searchModeOptions" :key="opt.value"
                                        :value="opt.value">
                                        {{ opt.label }}
                                    </a-select-option>
                                </a-select>
                            </div>
                        </template>

                        <template v-else-if="column.dataIndex === 'indexField'">
                            <div class="cell-stretch">
                                <a-select v-if="indexFieldOptionObjects.length" v-model:value="record.indexFieldList"
                                    mode="multiple" :options="indexFieldOptionObjects" placeholder="选择索引字段"
                                    class="multi-wrap" :allow-clear="false" :show-arrow="true"
                                    :dropdownMatchSelectWidth="false" :listHeight="320" :tagRender="renderTag"
                                    :showSearch="true" :filterOption="filterOption"
                                    @change="onIndexFieldChange(record)" />
                                <div v-else class="empty-select">
                                    <span class="tip">暂无可选字段</span>
                                    <a-button type="link" size="small" @click="reloadIndexFields">刷新</a-button>
                                </div>
                            </div>
                        </template>

                        <template v-else-if="column.key === 'ops'">
                            <a-button class="del-btn" size="small" type="text" danger
                                @click="removeRow(index)">删除</a-button>
                        </template>
                    </template>
                </a-table>

                <!-- 映射校验提示 -->
                <div v-if="mappingError" class="index-error-tip" ref="errorTipRef">
                    <i class="dot"></i>
                    <span>{{ mappingError }}</span>
                </div>

                <div class="index-add-row">
                    <div class="add-btn" @click="addRow">增加行</div>
                </div>
            </div>

            <!-- 分页参数 -->
            <a-form-item label="条数：" name="page_size" class="row-40">
                <a-input-number v-model:value="form.page_size" :min="1" :precision="0" style="width: 50%"
                    placeholder="每页条数" />
            </a-form-item>

            <a-form-item label="页数：" name="max_pages" class="row-40">
                <a-input-number v-model:value="form.max_pages" :min="1" :precision="0" style="width: 50%"
                    placeholder="最大页数" />
            </a-form-item>

            <!-- 算法配置 -->
            <a-form-item label="算法配置：" name="algoList" class="row-40" :required="true">
                <a-button @click="openAlgoCfg">选择</a-button>
            </a-form-item>

            <div v-if="algoList.length" class="algo-picked-wrap">
                <div v-for="(row, i) in algoList" :key="row.name" class="algo-picked-row">
                    <div class="algo-name">{{ row.name }}</div>
                    <a-select v-model:value="row.version" :options="versionOptionsMap[row.name] || algoVersionOptions"
                        @change="onAlgoListChange" />
                    <a-button class="del-btn" size="small" type="text" danger @click="removeAlgo(i)">删除</a-button>
                </div>
            </div>

            <!-- 词库配置 -->
            <a-form-item label="词库配置：" name="lexiconList" class="row-40" :required="true">
                <a-button @click="openLexiconCfg">选择</a-button>
            </a-form-item>

            <div v-if="lexiconList.length" class="lexicon-chips">
                <span v-for="it in lexiconList" :key="it" class="chip">
                    {{ it }}
                    <i class="x" role="button" aria-label="删除" title="删除" @click="removeLexicon(it)">×</i>
                </span>
            </div>

            <!-- 向量搜索配置 -->
            <a-form-item label="向量搜索配置：" class="row-40">
                <a-space>
                    <a-button type="primary" :ghost="form.vectorEnabled !== true"
                        @click="form.vectorEnabled = true">启用</a-button>
                    <a-button type="primary" :ghost="form.vectorEnabled !== false"
                        @click="form.vectorEnabled = false">停用</a-button>
                </a-space>
            </a-form-item>

            <div class="vector-card" v-show="form.vectorEnabled">
                <a-form-item label="召回算法：" name="recallMetric" class="row-40 inner">
                    <a-select v-model:value="form.recallMetric" style="width: 280px" :loading="recallLoading.algorithm"
                        placeholder="请选择">
                        <a-select-option v-for="opt in recallAlgorithmOptions" :key="opt.value" :value="opt.value">
                            {{ opt.value ? opt.label : '' }}
                        </a-select-option>
                    </a-select>
                </a-form-item>

                <a-form-item label="召回阈值：" name="recallDim" class="row-40 inner">
                    <a-select v-model:value="form.recallDim" style="width: 280px" :loading="recallLoading.threshold"
                        placeholder="请选择">
                        <a-select-option v-for="opt in recallThresholdOptions" :key="opt.value" :value="opt.value">
                            {{ opt.label }}
                        </a-select-option>
                    </a-select>
                </a-form-item>
            </div>

            <!-- 接口地址 -->
            <a-form-item v-if="isEdit" label="接口地址：" name="apiList" class="row-40" :required="true">
                <div class="api-card">
                    <div class="api-row" v-for="(m, i) in apiEndpointMetas" :key="i">
                        <code class="api-text">
                <strong>{{ m.method }}</strong>&nbsp;{{ m.url }}
            </code>
                        <a-space size="small" class="api-ops">
                            <a-button type="link" size="small" @click="testApi(m)">测试</a-button>
                            <a-button type="link" size="small" @click="copyApi(m.display)">复制</a-button>
                        </a-space>
                    </div>
                </div>
            </a-form-item>
        </a-form>

        <!-- 底部操作 -->
        <div class="page-actions">
            <!-- 接口预览 -->
            <a-button v-if="!isEdit" :loading="previewLoading" @click="openPreview">接口预览</a-button>
            <a-button type="primary" @click="submit">{{ isEdit ? '保存' : '创建' }}</a-button>
            <a-button @click="goBack">取消</a-button>
        </div>

        <!-- 索引弹窗 -->
        <a-modal v-model:visible="indexModalOpen" :title="null" :footer="null" :closable="false" width="560px" centered
            class="index-modal modal-narrow">
            <div class="im-head">
                <div class="im-title">索引列表</div>
                <a-button type="link" size="small" @click="indexModalOpen = false">关闭</a-button>
            </div>
            <a-radio-group v-model:value="indexModalSelectedId" class="im-list">
                <a-radio v-for="it in indexModalOptions" :key="it.id" :value="String(it.id)">{{ it.name }}</a-radio>
            </a-radio-group>
            <div class="im-footer">
                <a-space>
                    <a-button type="primary" @click="confirmIndexModal">确定</a-button>
                    <a-button @click="indexModalOpen = false">取消</a-button>
                </a-space>
            </div>
        </a-modal>

        <!-- 算法弹窗 -->
        <a-modal v-model:visible="algoModalOpen" :title="null" :footer="null" :closable="false" width="480px" centered
            class="algo-modal modal-narrow">
            <div class="im-head">
                <div class="im-title">算法模型列表</div>
                <a-button type="link" size="small" @click="algoModalOpen = false">关闭</a-button>
            </div>
            <a-checkbox-group v-model:value="algoModalChecked" class="algo-list">
                <a-checkbox v-for="it in algoCandidates" :key="it.code" :value="it.name">{{ it.name }}</a-checkbox>
            </a-checkbox-group>
            <div class="im-footer">
                <a-space>
                    <a-button type="primary" :loading="algoConfirmLoading" @click="confirmAlgoModal">确定</a-button>
                    <a-button @click="algoModalOpen = false">取消</a-button>
                </a-space>
            </div>
        </a-modal>

        <!-- 词库弹窗 -->
        <a-modal v-model:visible="lexiconModalOpen" :title="null" :footer="null" :closable="false" width="440px"
            centered class="lexicon-modal modal-narrow">
            <div class="im-head">
                <div class="im-title">词库列表</div>
                <a-button type="link" size="small" @click="lexiconModalOpen = false">关闭</a-button>
            </div>
            <a-checkbox-group v-model:value="lexiconModalChecked" class="lexicon-list">
                <a-checkbox v-for="it in lexiconCandidates" :key="it.code" :value="it.code">{{ it.value }}</a-checkbox>
            </a-checkbox-group>
            <div class="im-footer">
                <a-space>
                    <a-button type="primary" @click="confirmLexiconModal">确定</a-button>
                    <a-button @click="lexiconModalOpen = false">取消</a-button>
                </a-space>
            </div>
        </a-modal>

        <!-- 接口预览弹窗 -->
        <a-modal v-model:visible="previewOpen" title="接口预览" :footer="null" :closable="true" width="820px" centered
            :bodyStyle="{ overflow: 'hidden' }" class="modal-narrow">
            <div class="api-card api-card-preview">
                <div class="api-row-block" v-for="(row, i) in previewList" :key="i">
                    <!-- 接口类型 -->
                    <div class="api-type-line">
                        <span class="label">接口类型：</span>
                        <a-tag>{{ row.interface_method || '-' }}</a-tag>
                    </div>

                    <!-- 接口地址 -->
                    <div class="api-row">
                        <code class="api-text">{{ row.interface_url }}</code>
                    </div>

                    <!-- 接口参数 -->
                    <div class="api-params" v-if="row.params && Object.keys(row.params).length">
                        <div class="label">接口参数：</div>
                        <pre class="params-pre">{{ formatParams(row.params) }}</pre>
                    </div>
                </div>
            </div>
        </a-modal>

        <!-- 接口测试结果弹窗 -->
        <a-modal v-model:visible="testModalOpen" title="接口测试结果" :footer="null" :closable="true" width="820px" centered
            class="modal-narrow">
            <pre class="params-pre" style="max-height: 60vh; overflow: auto;">{{ testModalJson }}</pre>
        </a-modal>

        <div style="width: 100%; height: 10vh; background: transparent;"></div>
    </div>
</template>

<script setup lang="ts">
import { reactive, computed, watch, onMounted, ref, h, nextTick } from 'vue'
import type { Rule } from 'ant-design-vue'
import { message, Tag, Tooltip } from 'ant-design-vue'
import { useRouter, useRoute } from 'vue-router'
import { CloseOutlined } from '@ant-design/icons-vue'
import api from '@/api'

const router = useRouter()
const route = useRoute()
const formRef = ref()

const mode = computed(() => (route.query.mode === 'edit' ? 'edit' : 'create'))
const isEdit = computed(() => mode.value === 'edit')
const pageTitle = computed(() => (isEdit.value ? '编辑' : '新增'))

type AlgoPicked = { name: string; version: string }
const algoListRef = ref<AlgoPicked[]>([])
const lexiconListRef = ref<string[]>([])
const lexiconCodePicked = ref<string[]>([])
const algoList = computed(() => algoListRef.value)
const lexiconList = computed(() => lexiconListRef.value)

type Option = { label: string; value: string }
type AlgoMeta = { code: string; name: string }

const algoCandidates = ref<AlgoMeta[]>([])
const algoListLoading = ref(false)
const algoConfirmLoading = ref(false)
const algoCodeMapByName = ref<Record<string, string>>({})
const versionOptionsMap = ref<Record<string, Option[]>>({})

const algoVersionOptions = [
    { label: '默认算法 v1.0.0.13', value: '默认算法 v1.0.0.13' },
    { label: '默认算法 v1.0.0.12', value: '默认算法 v1.0.0.12' },
    { label: '默认算法 v1.0.0.11', value: '默认算法 v1.0.0.11' },
    { label: '默认算法 v1.0.0.10', value: '默认算法 v1.0.0.10' }
]
function extractVersionCode(versionName: string) {
    const m = versionName.match(/(v\d+\.\d+\.\d+\.\d+)/i)
    return m ? m[1] : versionName
}

/** （保留） */
function normalizeEnabled(v: any): boolean {
    if (typeof v === 'boolean') return v
    if (v == null) return false
    if (typeof v === 'number') return v !== 0
    if (typeof v === 'string') {
        const s = v.trim().toLowerCase()
        if (s === 'true') return true
        if (s === 'false') return false
        return !!s
    }
    if (typeof v === 'object') {
        const e = (v as any).enabled ?? (v as any).enable ?? (v as any).is_enabled ?? (v as any).open ?? (v as any).status
        if (e !== undefined) return normalizeEnabled(e)
        return Object.keys(v).length > 0
    }
    return !!v
}

/**
 * ========================
 * 字典区
 * ========================
 */
const apiTypeOptions = ref<Option[]>([])
const inputTypeOptions = ref<Option[]>([])
const dataTypeOptions = ref<Option[]>([])
const dictLoading = reactive({ apiType: false, inputType: false, dataType: false })

const searchModeLoading = ref(false)
const searchModeOptions = ref<Option[]>([])

const recallAlgorithmOptions = ref<Option[]>([])
const recallThresholdOptions = ref<Option[]>([])
const recallLoading = reactive({ algorithm: false, threshold: false })

function getDefaultSearchModeValue() {
    return searchModeOptions.value.length ? String(searchModeOptions.value[0].value) : ''
}

/**
 * ========================
 * 表单模型
 * ========================
 */
const form = reactive({
    id: '' as string | number,
    name: '',
    enabled: 'enabled',
    public: 'public',
    apiType: undefined as string | undefined,
    inputType: undefined as string | undefined,
    dataType: undefined as string | undefined,
    indexBinding: '' as string,
    // —— 分页字段 ——
    page_size: undefined as number | undefined,
    max_pages: undefined as number | undefined,
    size: undefined as number | undefined,
    page: undefined as number | undefined,

    algoCfg: true,
    lexiconCfg: true,
    vectorEnabled: true,
    recallMetric: '' as string | undefined,
    recallDim: '' as string | undefined,
    algoList: [] as { name: string; version: string }[],
    lexiconList: [] as string[],
    apiList: [] as string[]
})

watch(() => form.page_size, v => { form.size = v as any })
watch(() => form.max_pages, v => { form.page = v as any })

/** 编辑态 API 展示 */
const apiEndpoints = ref<string[]>([])
const apiEndpointMetas = ref<Array<{ display: string; method: string; url: string; params: Record<string, any> }>>([])

/**
 * ========================
 * 索引字段
 * ========================
 */
const indexFieldOptions = ref<string[]>([])
const indexFieldOptionObjects = computed(() => indexFieldOptions.value.map(v => ({ label: v, value: v })))

function renderTag(props: any) {
    const { label, closable, onClose } = props
    return h(Tooltip, { title: String(label) }, {
        default: () => h(Tag, {
            closable, onClose,
            style: 'max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-right:4px;'
        }, () => String(label))
    })
}
function filterOption(input: string, option: any) {
    const txt = String(option?.label ?? option?.value ?? '').toLowerCase()
    return txt.includes(input.toLowerCase())
}

/**
 * ========================
 * 顶层校验规则
 * ========================
 */
const rules: Record<string, Rule[]> = {
    name: [{ required: true, message: '请输入接口名称' }],
    enabled: [{ required: true }],
    public: [{ required: true }],
    apiType: [{ required: true, message: '请选择接口类型' }],
    inputType: [{ required: true, message: '请选择输入类型' }],
    dataType: [{ required: true, message: '请选择数据类型' }],
    indexBinding: [{ required: true, message: '请选择索引关联' }],
    algoList: [
        { validator: () => (algoListRef.value.length > 0 ? Promise.resolve() : Promise.reject('请选择算法模型')), trigger: 'change' }
    ],
    lexiconList: [
        { validator: () => (lexiconListRef.value.length > 0 ? Promise.resolve() : Promise.reject('请选择词库')), trigger: 'change' }
    ],
    apiList: [
        { validator: () => (!isEdit.value || apiEndpoints.value.length > 0 ? Promise.resolve() : Promise.reject('请配置接口地址')), trigger: 'change' }
    ],
    page_size: [{ required: true, message: '请填写 Size' }],
    max_pages: [{ required: true, message: '请填写 Page' }],
}

/**
 * ========================
 * 字典拉取
 * ========================
 */
function mapDict(res: any): Option[] {
    const d = res?.data
    if (Array.isArray(d) && d.length > 0) {
        if (typeof d[0] === 'string') return d.map((s: string) => ({ label: s, value: s }))
        return d.map((it: any) => ({ label: String(it?.value ?? it?.label ?? it?.code ?? ''), value: String(it?.code ?? it?.value ?? '') }))
    }
    return []
}
function mapDictSearchMode(res: any): Option[] {
    const d = res?.data
    if (Array.isArray(d) && d.length > 0) {
        if (typeof d[0] === 'string') {
            return d.map((s: string) => {
                const codeGuess = s === '关键词' ? 'keyword' : s === '短语' ? 'phrase' : s.toLowerCase()
                return { label: s, value: codeGuess }
            })
        }
        return d.map((it: any) => ({ label: String(it?.value ?? it?.label ?? ''), value: String(it?.code ?? it?.value ?? '') }))
    }
    return []
}

async function loadApiTypeOptions() {
    dictLoading.apiType = true
    try { apiTypeOptions.value = mapDict(await (api as any).dictionaryInterfaceType()) }
    catch { apiTypeOptions.value = []; message.error('接口类型字典获取失败') }
    finally { dictLoading.apiType = false }
}
async function loadInputTypeOptions() {
    dictLoading.inputType = true
    try { inputTypeOptions.value = mapDict(await (api as any).dictionaryInputType()) }
    catch { inputTypeOptions.value = []; message.error('输入类型字典获取失败') }
    finally { dictLoading.inputType = false }
}
async function loadDataTypeOptions() {
    dictLoading.dataType = true
    try { dataTypeOptions.value = mapDict(await (api as any).dictionaryDataType()) }
    catch { dataTypeOptions.value = []; message.error('数据类型字典获取失败') }
    finally { dictLoading.dataType = false }
}
async function loadSearchModeOptions() {
    searchModeLoading.value = true
    try { searchModeOptions.value = mapDictSearchMode(await (api as any).dictionarySearchMode()) }
    catch { searchModeOptions.value = []; message.error('搜索方式字典获取失败') }
    finally { searchModeLoading.value = false; ensureDefaultSearchMode(); tryHideMappingError() }
}

async function loadRecallAlgorithmOptions() {
    recallLoading.algorithm = true
    try {
        recallAlgorithmOptions.value = mapDict(await (api as any).dictionaryRecallAlgorithm())
        if (!form.recallMetric && recallAlgorithmOptions.value.length) form.recallMetric = String(recallAlgorithmOptions.value[0].value)
    } catch { recallAlgorithmOptions.value = []; message.error('召回算法获取失败') }
    finally { recallLoading.algorithm = false }
}
async function loadRecallThresholdOptions() {
    recallLoading.threshold = true
    try {
        recallThresholdOptions.value = mapDict(await (api as any).dictionaryRecallThreshold())
        if (!form.recallDim && recallThresholdOptions.value.length) form.recallDim = String(recallThresholdOptions.value[0].value)
    } catch { recallThresholdOptions.value = []; message.error('召回阈值获取失败') }
    finally { recallLoading.threshold = false }
}
async function loadAllDictionaries() {
    await Promise.all([loadApiTypeOptions(), loadInputTypeOptions(), loadDataTypeOptions(), loadRecallAlgorithmOptions(), loadRecallThresholdOptions()])
}

/**
 * ========================
 * 初始化 / 编辑态加载
 * ========================
 */
function setDefaultApis() {
    if (apiEndpoints.value.length) return
    apiEndpoints.value = [
        'GET  /api/news/search?keyword={user_keyword}&page={page_number}&size={results_per_page}',
        'POST /api/news/search?keyword={user_keyword}&page={page_number}&size={results_per_page}'
    ]
    form.apiList = [...apiEndpoints.value]
}

const rowSeq = ref(0)
const nextKey = () => `row_${++rowSeq.value}`

function resetForCreate() {
    Object.assign(form, {
        id: '',
        name: '',
        enabled: 'enabled',
        public: 'public',
        apiType: undefined,
        inputType: undefined,
        dataType: undefined,
        indexBinding: '',
        page_size: undefined,
        max_pages: undefined,
        size: undefined,
        page: undefined,
        algoCfg: true,
        lexiconCfg: true,
        vectorEnabled: true,
        recallMetric: '',
        recallDim: '',
        algoList: [],
        lexiconList: [],
        apiList: []
    })
    clearIndexBinding()
    algoListRef.value = []
    lexiconListRef.value = []
    lexiconCodePicked.value = []
    apiEndpoints.value = []
    apiEndpointMetas.value = []
    form.apiList = []
    rowSeq.value = 0
}

/* 默认一行 */
function buildDefaultMapping() {
    const defMode = getDefaultSearchModeValue()
    mappingRows.value = [{ key: nextKey(), searchField: 'title', searchMode: defMode, indexField: '', indexFieldList: [] }]
}
function ensureDefaultSearchMode() {
    const def = getDefaultSearchModeValue()
    if (!def) return
    mappingRows.value = mappingRows.value.map(r => ({ ...r, searchMode: r.searchMode || def }))
}

/* —— 编辑态详情 —— */

async function ensureIndexName() {
    if (selectedIndexId.value === null || selectedIndexId.value === undefined) {
        form.indexBinding = ''
        return
    }
    if (!indexModalOptions.value.length) {
        try { indexModalOptions.value = normalizeIndexList(await (api as any).getIndexList()) }
        catch { indexModalOptions.value = [] }
    }
    const picked = indexModalOptions.value.find(i => String(i.id) === String(selectedIndexId.value))
    form.indexBinding = picked?.name || ''
}

async function loadForEdit(id: string | number) {
    try {
        // 搜索方式
        await loadSearchModeOptions()

        const res: any = await (api as any).getSearchInterfaceById({ id })
        const detail = res?.data ?? res
        if (!detail || typeof detail !== 'object') throw new Error('未获取到有效详情')

        // 基础信息
        form.id = detail.id ?? id
        form.name = String(detail.interface_name ?? '')
        form.enabled = detail.interface_enable ? 'enabled' : 'disabled'
        form.public = String(detail.interface_open ?? 'public')
        form.apiType = String(detail.interface_type ?? '')
        form.inputType = String(detail.input_type ?? '')
        form.dataType = String(detail.data_type ?? '')

        const vsc = detail.vector_search_config
        form.vectorEnabled = (typeof vsc === 'boolean') ? vsc : String(vsc).toLowerCase() === 'true'

        form.recallMetric = detail.recall_algorithm ? String(detail.recall_algorithm) : ''
        form.recallDim = detail.recall_threshold ? String(detail.recall_threshold) : ''

        // 分页
        const ps = detail.page_size ?? detail.size
        const mp = detail.max_pages ?? detail.page
        form.page_size = ps != null ? Number(ps) : undefined
        form.max_pages = mp != null ? Number(mp) : undefined
        form.size = form.page_size as any
        form.page = form.max_pages as any

        // 词库回显
        await loadLexiconCandidates()
        if (Array.isArray(detail.dictionary_config)) {
            const rawArr = detail.dictionary_config.map((x: any) => {
                if (typeof x === 'string') return x
                return String(x?.code ?? x?.value ?? '')
            }).filter(Boolean)

            const codeByName = new Map(lexiconCandidates.value.map(i => [i.value, i.code]))
            const nameByCode = new Map(lexiconCandidates.value.map(i => [i.code, i.value]))

            const codes = rawArr.map(s => codeByName.get(String(s)) || String(s))
            lexiconCodePicked.value = Array.from(new Set(codes))
            lexiconListRef.value = lexiconCodePicked.value.map(c => nameByCode.get(c) || c)
            form.lexiconList = [...lexiconListRef.value]
        } else {
            lexiconCodePicked.value = []; lexiconListRef.value = []; form.lexiconList = []
        }

        // 算法
        if (Array.isArray(detail.algorithm_config)) {
            algoListRef.value = detail.algorithm_config
                .map((it: any) => ({ name: String(it?.algorithm_name || ''), version: String(it?.algorithm_version_name || '') }))
                .filter(a => a.name)
            form.algoList = [...algoListRef.value]
            algoCodeMapByName.value = {}
            detail.algorithm_config.forEach((it: any) => {
                const name = String(it?.algorithm_name || '')
                const code = String(it?.algorithm_code || '')
                if (name && code) algoCodeMapByName.value[name] = code
            })
        } else {
            algoListRef.value = []; form.algoList = []; algoCodeMapByName.value = {}
        }

        if (detail.index_rel !== undefined && detail.index_rel !== null) {
            selectedIndexId.value = String(detail.index_rel)
            await ensureIndexName()
        } else {
            form.indexBinding = ''
            selectedIndexId.value = null
        }

        // 索引映射
        if (Array.isArray(detail.index_list) && detail.index_list.length) {
            const def = getDefaultSearchModeValue()
            mappingRows.value = detail.index_list.map((r: any) => {
                const raw = r?.index_field ?? r?.indexField ?? ''
                const list = Array.isArray(raw) ? raw : String(raw).split(',').map((s: string) => s.trim()).filter(Boolean)
                let mode = String(r?.search_mode ?? r?.searchMode ?? '').trim()
                const hit = searchModeOptions.value.find(o => o.value === mode || o.label === mode || o.value === mode.toLowerCase())
                if (hit) mode = hit.value
                else if (!mode) mode = def
                return { key: nextKey(), searchField: r?.search_field ?? r?.searchField ?? '', searchMode: mode, indexField: Array.isArray(raw) ? raw.join(',') : String(raw), indexFieldList: list }
            })
        } else {
            buildDefaultMapping()
        }

        if (selectedIndexId.value !== null && selectedIndexId.value !== undefined) {
            const used = new Set<string>()
            mappingRows.value.forEach(r => (r.indexFieldList || []).forEach(f => used.add(String(f)))
            )
            indexFieldOptions.value = Array.from(used)
            await loadIndexFieldOptions(selectedIndexId.value as string)
            if (indexFieldOptions.value.length || used.size) {
                const merged = new Set(indexFieldOptions.value)
                used.forEach(v => merged.add(v))
                indexFieldOptions.value = Array.from(merged)
            }
        }

        /** ================= 接口地址 ================= */
        const listRaw = Array.isArray(detail?.interface_url_list)
            ? detail.interface_url_list
            : (detail?.interface_url || detail?.interface_method || detail?.test_params)
                ? [{ interface_url: detail.interface_url, interface_method: detail.interface_method, test_params: detail.test_params }]
                : []

        apiEndpointMetas.value = (listRaw || []).map((it: any) => {
            const method = String(it?.interface_method || it?.method || '').toUpperCase()
            const url = String(it?.interface_url || it?.url || '')
            const params = (it?.test_params || it?.params || {}) as Record<string, any>
            const display = `${method.padEnd(4, ' ')} ${url}`
            return { display, method, url, params }
        }).filter(x => x.method && x.url)

        apiEndpoints.value = apiEndpointMetas.value.map(m => m.display)
        form.apiList = [...apiEndpoints.value]
        /** ================= 接口地址对接 ================= */

        setDefaultApis()

        nextTick(() => {
            formRef.value?.validateFields([
                'name', 'enabled', 'public', 'apiType', 'inputType', 'dataType', 'lexiconList', 'algoList', 'recallMetric', 'recallDim',
                'page_size', 'max_pages'
            ]).catch(() => { })
        })
    } catch (e: any) {
        message.error(e?.message || '加载编辑详情失败')
        if (!mappingRows.value.length) buildDefaultMapping()
    }
}

/** 路由 */
function syncByRoute() {
    if (isEdit.value) {
        const id = (route.query.id as string) || ''
        if (!id) { message.warning('缺少编辑ID，已回到列表'); router.back(); return }
        form.id = id
        loadForEdit(id)
    } else {
        resetForCreate()
    }
}
onMounted(async () => { await loadAllDictionaries(); syncByRoute() })
watch(() => route.fullPath, syncByRoute)
function goBack() { router.back() }

/**
 * ========================
 * 索引映射区
 * ========================
 */
const mappingError = ref('')
const errorTipRef = ref<HTMLDivElement | null>(null)
function showMappingError(msg: string) {
    mappingError.value = msg
    message.error(msg)
    nextTick(() => { errorTipRef.value?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }) })
}
function rowsAllValid(): boolean {
    if (!selectedIndexId.value && selectedIndexId.value !== '0') return false
    if (!mappingRows.value.length) return false
    for (const r of mappingRows.value) {
        if (!r.searchField || !String(r.searchField).trim()) return false
        if (!r.searchMode) return false
        if (!Array.isArray(r.indexFieldList) || r.indexFieldList.length === 0) return false
    }
    return true
}
function tryHideMappingError() { if (mappingError.value && rowsAllValid()) mappingError.value = '' }

function validateMappingRows(): boolean {
    if (!selectedIndexId.value && selectedIndexId.value !== '0') { showMappingError('请先选择索引关联'); return false }
    if (!mappingRows.value.length) { showMappingError('请至少添加一条索引映射行'); return false }
    const def = getDefaultSearchModeValue()
    for (let i = 0; i < mappingRows.value.length; i++) {
        const r = mappingRows.value[i]
        if (!r.searchMode && def) r.searchMode = def
        if (!r.searchField || !String(r.searchField).trim()) { showMappingError(`第 ${i + 1} 行：请填写「搜索字段」`); return false }
        if (!r.searchMode) { showMappingError(`第 ${i + 1} 行：请选择「搜索方式」`); return false }
        if (!Array.isArray(r.indexFieldList) || r.indexFieldList.length === 0) { showMappingError(`第 ${i + 1} 行：请选择至少一个「索引字段」`); return false }
    }
    mappingError.value = ''
    return true
}

/**
 * ========================
 * 提交
 * ========================
 */
async function submit() {
    try { await formRef.value?.validate() }
    catch { message.error('请完善表单必填项'); return }
    if (!validateMappingRows()) return

    const index_list = mappingRows.value.map(r => {
        const list = Array.isArray(r.indexFieldList) ? r.indexFieldList :
            r.indexField ? String(r.indexField).split(',').map(s => s.trim()).filter(Boolean) : []
        return { search_field: r.searchField || '', search_mode: String(r.searchMode || ''), index_field: list }
    })

    const algorithm_config = form.algoList.map(a => ({
        algorithm_code: (algoCodeMapByName.value[a.name] || a.name),
        algorithm_name: a.name,
        algorithm_version_code: extractVersionCode(a.version),
        algorithm_version_name: a.version
    }))
    const interfaceUrlListWhenCreate = [
        {},
    ]
    const addPayload = {
        interface_name: form.name,
        interface_enable: form.enabled === 'enabled',
        interface_open: form.public,
        interface_type: form.apiType,
        input_type: form.inputType,
        data_type: form.dataType,
        algorithm_config,
        dictionary_config: [...lexiconCodePicked.value],
        vector_search_config: !!form.vectorEnabled,
        recall_algorithm: String(form.recallMetric || ''),
        recall_threshold: String(form.recallDim || ''),
        index_rel: selectedIndexId.value,
        index_list,
        page_size: form.page_size,
        max_pages: form.max_pages,
        size: form.page_size,
        page: form.max_pages,
        ...(!isEdit.value ? { interface_url_list: interfaceUrlListWhenCreate } : {})
    }

    const updatePayload = { interface_id: form.id, ...addPayload }

    try {
        let res: any
        if (isEdit.value) res = await (api as any).updateSearchInterface(updatePayload)
        else res = await (api as any).addSearchInterface(addPayload)
        if (res?.code !== 1000) throw new Error(res?.msg || '提交失败')
        message.success(isEdit.value ? `保存成功（ID: ${form.id}）` : '创建成功')
        router.back()
    } catch (e: any) {
        message.error(e?.message || '提交失败')
    }
}

/**
 * ========================
 * 索引弹窗/字段候选
 * ========================
 */
const indexModalOpen = ref(false)
const indexModalSelectedId = ref<string | null>(null)
const indexModalOptions = ref<Array<{ id: string; name: string }>>([])
const selectedIndexId = ref<string | null>(null)

const indexLinked = computed(() => !!(form.indexBinding || selectedIndexId.value))

function normalizeIndexList(res: any): Array<{ id: string; name: string }> {
    const list = Array.isArray(res?.data) ? res.data : []
    return list.map((it: any) => ({ id: String(it?.index_id), name: it?.index_name ?? '' }))
}
async function loadIndexList() {
    try { indexModalOptions.value = normalizeIndexList(await (api as any).getIndexList()) }
    catch { indexModalOptions.value = []; message.error('索引列表获取失败') }
}
function openIndexModal() {
    indexModalOpen.value = true
    indexModalSelectedId.value = selectedIndexId.value ?? null
    if (!indexModalOptions.value.length) loadIndexList()
}
async function confirmIndexModal() {
    if (!indexModalSelectedId.value && indexModalSelectedId.value !== '0') { message.warning('请选择一个索引'); return }
    selectedIndexId.value = String(indexModalSelectedId.value)
    const picked = indexModalOptions.value.find(i => String(i.id) === String(selectedIndexId.value))
    form.indexBinding = picked?.name || ''
    indexModalOpen.value = false
    await loadSearchModeOptions()
    await loadIndexRelAndFill(selectedIndexId.value as string)
    await loadIndexFieldOptions(selectedIndexId.value as string)
    if (!mappingRows.value.length) buildDefaultMapping()
    tryHideMappingError()
}

watch(selectedIndexId, (v) => {
    indexModalSelectedId.value = v ?? null
})

async function loadIndexRelAndFill(id: string) {
    try {
        const res = await (api as any).getIndexRelById({ id })
        const rows = Array.isArray(res?.data?.index_list) ? res.data.index_list : Array.isArray(res?.data) ? res.data : []
        const mapped = rows.map((r: any) => {
            const raw = r?.index_field ?? r?.indexField ?? ''
            const list = Array.isArray(raw) ? raw : String(raw).split(',').map((s: string) => s.trim()).filter(Boolean)
            let mode = String(r?.search_mode ?? r?.searchMode ?? '').trim()
            const hit = searchModeOptions.value.find(o => o.value === mode || o.label === mode || o.value === mode.toLowerCase())
            if (hit) mode = hit.value
            else if (!mode) mode = getDefaultSearchModeValue()
            return { key: nextKey(), searchField: r?.search_field ?? r?.searchField ?? '', searchMode: mode, indexField: Array.isArray(raw) ? raw.join(',') : String(raw), indexFieldList: list }
        })
        mappingRows.value = mapped
        ensureDefaultSearchMode()
        tryHideMappingError()
    } catch { message.error('索引关联详情获取失败'); mappingRows.value = [] }
}

async function loadIndexFieldOptions(id: string) {
    try {
        const res = await (api as any).getIndexFieldListById({ id })
        const arr = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.list) ? res.data.list : []
        const names = (arr || [])
            .map((s: any) => String(typeof s === 'string' ? s : s?.field_name ?? s?.name ?? s?.value ?? '').trim())
            .filter(Boolean)
        indexFieldOptions.value = Array.from(new Set(names))
        if (!mappingRows.value.length && indexFieldOptions.value.length) {
            const defMode = getDefaultSearchModeValue()
            mappingRows.value = indexFieldOptions.value.map(f => ({ key: nextKey(), searchField: f, searchMode: defMode, indexField: f, indexFieldList: [f] }))
        }
        tryHideMappingError()
    } catch { indexFieldOptions.value = []; message.error('索引字段获取失败') }
}
function reloadIndexFields() {
    if (!selectedIndexId.value && selectedIndexId.value !== '0') { message.warning('请先选择索引'); return }
    loadIndexFieldOptions(selectedIndexId.value as string)
}

/**
 * ========================
 * 映射表本地操作
 * ========================
 */
type MappingRow = { key: string; searchField: string; searchMode: string; indexField: string; indexFieldList: string[]; _editable?: boolean }
const mappingRows = ref<MappingRow[]>([])
const mappingColumns = [
    { title: '搜索字段', dataIndex: 'searchField', width: 240 },
    { title: '搜索方式（单选）', dataIndex: 'searchMode', width: 280 },
    { title: '索引字段', dataIndex: 'indexField', width: 560 },
    { title: '操作', key: 'ops', width: 90 }
]
function onCellBlur() { tryHideMappingError() }
function removeRow(i: number) { mappingRows.value.splice(i, 1); tryHideMappingError() }
function addRow() {
    const defMode = getDefaultSearchModeValue()
    mappingRows.value.push({ key: nextKey(), searchField: '', searchMode: defMode, indexField: '', indexFieldList: [], _editable: true })
    tryHideMappingError()
}
function onSearchModeChange() { tryHideMappingError() }
function onIndexFieldChange(row: MappingRow) {
    row.indexField = (row.indexFieldList || []).join(',')
        ; (row.indexFieldList || []).forEach(v => { if (v && !indexFieldOptions.value.includes(v)) indexFieldOptions.value.push(v) })
    tryHideMappingError()
}

/**
 * ========================
 * 算法/词库 弹窗操作
 * ========================
 */
const algoModalOpen = ref(false)
const algoModalChecked = ref<string[]>([])

async function openAlgoCfg() {
    algoModalChecked.value = algoListRef.value.map(i => i.name)
    algoModalOpen.value = true
    if (algoCandidates.value.length) return
    try {
        algoListLoading.value = true
        const res: any = await (api as any).getAlgorithmList()
        const list = Array.isArray(res?.data) ? res.data : []
        algoCandidates.value = list.map((it: any) => ({ code: String(it?.algorithm_code ?? ''), name: String(it?.algorithm_name ?? '') })).filter(a => a.code && a.name)
        algoCodeMapByName.value = Object.fromEntries(algoCandidates.value.map(a => [a.name, a.code]))
    } catch { algoCandidates.value = []; algoCodeMapByName.value = {}; message.error('算法列表获取失败') }
    finally { algoListLoading.value = false }
}

async function confirmAlgoModal() {
    const pickedNames = Array.from(new Set(algoModalChecked.value))
    const next: AlgoPicked[] = []
    pickedNames.forEach(n => { const exist = algoListRef.value.find(x => x.name === n); next.push({ name: n, version: exist?.version || '' }) })
    try {
        algoConfirmLoading.value = true
        const codes = pickedNames.map(n => algoCodeMapByName.value[n]).filter(Boolean)
        if (codes.length) {
            const res: any = await (api as any).getAlgorithmVersions({ algorithm_codes: codes })
            const arr = Array.isArray(res?.data) ? res.data : []
            const tmpMap: Record<string, Option[]> = {}
            arr.forEach((item: any) => {
                const name = String(item?.algorithm_name ?? '')
                const vlist = Array.isArray(item?.algorithm_version_list) ? item.algorithm_version_list : []
                const options: Option[] = vlist.map((v: any) => ({ label: String(v?.algorithm_version_name ?? ''), value: String(v?.algorithm_version_name ?? '') })).filter(op => op.label && op.value)
                if (name) tmpMap[name] = options
            })
            versionOptionsMap.value = tmpMap
        } else {
            versionOptionsMap.value = {}
        }
        next.forEach(row => {
            if (!row.version) {
                const own = versionOptionsMap.value[row.name]
                if (own && own.length) row.version = own[0].value as string
                else if (algoVersionOptions.length) row.version = algoVersionOptions[0].value as string
            }
        })
        algoListRef.value = next
        form.algoList = [...algoListRef.value]
        formRef.value?.validateFields(['algoList']).catch(() => { })
        algoModalOpen.value = false
    } catch { message.error('算法版本获取失败') }
    finally { algoConfirmLoading.value = false }
}
function removeAlgo(i: number) { algoListRef.value.splice(i, 1); form.algoList = [...algoListRef.value]; formRef.value?.validateFields(['algoList']).catch(() => { }) }
function onAlgoListChange() { form.algoList = [...algoListRef.value]; formRef.value?.validateFields(['algoList']).catch(() => { }) }

/**
 * ========================
 * 词库 弹窗
 * ========================
 */
const lexiconModalOpen = ref(false)
const lexiconCandidates = ref<Array<{ code: string; value: string }>>([])
const lexiconModalChecked = ref<string[]>([])

async function loadLexiconCandidates() {
    try {
        const res: any = await (api as any).dictionaryLexicon()
        const arr = Array.isArray(res?.data) ? res.data : []
        lexiconCandidates.value = arr.map((it: any) => ({ code: String(it?.code ?? ''), value: String(it?.value ?? '') })).filter(x => x.code && x.value)
    } catch { lexiconCandidates.value = []; message.error('词库列表获取失败') }
}
function openLexiconCfg() {
    if (!lexiconCandidates.value.length) loadLexiconCandidates()
    lexiconModalChecked.value = [...lexiconCodePicked.value]
    lexiconModalOpen.value = true
}
function confirmLexiconModal() {
    lexiconCodePicked.value = [...new Set(lexiconModalChecked.value)]
    const map = new Map(lexiconCandidates.value.map(i => [i.code, i.value]))
    lexiconListRef.value = lexiconCodePicked.value.map(c => map.get(c)!).filter(Boolean)
    form.lexiconList = [...lexiconListRef.value]
    lexiconModalOpen.value = false
    formRef.value?.validateFields(['lexiconList']).catch(() => { })
}
function removeLexicon(name: string) {
    const idx = lexiconListRef.value.indexOf(name)
    if (idx !== -1) {
        const mapReverse = new Map(lexiconCandidates.value.map(i => [i.value, i.code]))
        const code = mapReverse.get(name)
        if (code) {
            const cidx = lexiconCodePicked.value.indexOf(code)
            if (cidx !== -1) lexiconCodePicked.value.splice(cidx, 1)
        }
        lexiconListRef.value.splice(idx, 1)
        form.lexiconList = [...lexiconListRef.value]
        formRef.value?.validateFields(['lexiconList']).catch(() => { })
    }
}

/**
 * 工具方法
 */
const testModalOpen = ref(false)
const testModalData = ref<any>(null)
const testModalJson = computed(() => {
    try { return JSON.stringify(testModalData.value ?? {}, null, 2) } catch { return String(testModalData.value) }
})

async function testApi(metaOrText: any) {
    const meta = typeof metaOrText === 'string'
        ? apiEndpointMetas.value.find(m => m.display === metaOrText)
        : metaOrText

    if (!meta) { message.warning('未找到该接口的测试信息'); return }

    try {
        const payload = { interface_id: form.id, ...(meta.params || {}) }
        const res: any = await (api as any).testInterface(payload)
        testModalData.value = res
        testModalOpen.value = true
    } catch (e: any) {
        testModalData.value = { error: e?.message || '测试调用异常' }
        testModalOpen.value = true
    }
}
async function copyApi(text: string) {
    try { await navigator.clipboard.writeText(text); message.success('已复制到剪贴板') }
    catch { message.error('复制失败，请手动选中复制') }
}
function clearIndexBinding() {
    form.indexBinding = ''
    selectedIndexId.value = null
    mappingRows.value = []
    indexFieldOptions.value = []
    rowSeq.value = 0
    mappingError.value = ''
}

/**
 * ========================
 * 接口预览
 * ========================
 */
const previewOpen = ref(false)
const previewLoading = ref(false)
type PreviewRow = { interface_url: string; interface_method: string; params?: Record<string, any> }
const previewList = ref<PreviewRow[]>([])

function formatParams(p: Record<string, any>) {
    try { return JSON.stringify(p, null, 2) } catch { return String(p) }
}

async function openPreview() {
    const index_list = mappingRows.value.map(r => {
        const list = Array.isArray(r.indexFieldList) ? r.indexFieldList :
            r.indexField ? String(r.indexField).split(',').map(s => s.trim()).filter(Boolean) : []
        return { search_field: r.searchField || '', search_mode: String(r.searchMode || ''), index_field: list }
    })
    const algorithm_config = form.algoList.map(a => ({
        algorithm_code: (algoCodeMapByName.value[a.name] || a.name),
        algorithm_name: a.name,
        algorithm_version_code: extractVersionCode(a.version),
        algorithm_version_name: a.version
    }))
    const interface_url_list = [
        { interface_url: 'https://example.com/api1', interface_method: 'GET' },
        { interface_url: 'https://example.com/api1', interface_method: 'POST' }
    ]
    const payload = {
        interface_name: form.name,
        interface_enable: form.enabled === 'enabled',
        interface_open: form.public,
        interface_type: form.apiType,
        input_type: form.inputType,
        data_type: form.dataType,
        algorithm_config,
        dictionary_config: [...lexiconCodePicked.value],
        vector_search_config: !!form.vectorEnabled,
        recall_algorithm: String(form.recallMetric || ''),
        recall_threshold: String(form.recallDim || ''),
        index_rel: selectedIndexId.value,
        index_list,
        page_size: form.page_size,
        max_pages: form.max_pages,
        size: form.page_size,
        page: form.max_pages,
        interface_url_list
    }

    try {
        previewLoading.value = true
        const res: any = await (api as any).getPreviewData(payload)
        if (res?.code === 1001) {
            message.warning('请填写必填参数')
            return
        }
        if (res?.code !== 1000) {
            message.error(res?.msg || '接口预览失败')
            return
        }

        const listRaw = Array.isArray(res?.data?.interface_url_list)
            ? res.data.interface_url_list
            : Array.isArray(res?.data) ? res.data : []

        previewList.value = listRaw.map((it: any) => ({
            interface_url: String(it?.interface_url ?? it?.url ?? ''),
            interface_method: String(it?.interface_method ?? it?.method ?? ''),
            params: (it?.test_params ?? it?.params ?? {}) as Record<string, any>
        }))

        previewOpen.value = true
    } catch (e: any) {
        message.error(e?.message || '接口预览失败')
    } finally {
        previewLoading.value = false
    }
}
</script>

<style lang="less" scoped>
.page {
    min-height: 100vh;
    max-height: 100vh;
    overflow: auto;
    scrollbar-gutter: stable;
    padding: 8px 12px 24px 12px;
    background: #fff;
}

.page {
    scrollbar-width: thin;
    scrollbar-color: #c7d1e6 #f2f4f8;
}

.page::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

.page::-webkit-scrollbar-track {
    background: #f2f4f8;
    border-radius: 10px;
}

.page::-webkit-scrollbar-thumb {
    background: linear-gradient(180deg, #cfd8ea, #b8c5e4);
    border-radius: 10px;
    border: 2px solid #f2f4f8;

}

.page::-webkit-scrollbar-thumb:hover {
    background: linear-gradient(180deg, #b9c6e5, #a7b6df);
}

.page::-webkit-scrollbar-thumb:active {
    background: #96a6d8;
}

.index-modal .im-list,
.algo-modal .algo-list,
.lexicon-modal .lexicon-list,
.api-card-preview,
.params-pre,
.mapping-table :deep(.ant-table-body) {
    scrollbar-width: thin;
    scrollbar-color: #c7d1e6 #f2f4f8;
}

.index-modal .im-list::-webkit-scrollbar,
.algo-modal .algo-list::-webkit-scrollbar,
.lexicon-modal .lexicon-list::-webkit-scrollbar,
.api-card-preview::-webkit-scrollbar,
.params-pre::-webkit-scrollbar,
.mapping-table :deep(.ant-table-body)::-webkit-scrollbar {
    width: 8px;
    height: 8px;
}

.index-modal .im-list::-webkit-scrollbar-track,
.algo-modal .algo-list::-webkit-scrollbar-track,
.lexicon-modal .lexicon-list::-webkit-scrollbar-track,
.api-card-preview::-webkit-scrollbar-track,
.params-pre::-webkit-scrollbar-track,
.mapping-table :deep(.ant-table-body)::-webkit-scrollbar-track {
    background: #f2f4f8;
    border-radius: 8px;
}

.index-modal .im-list::-webkit-scrollbar-thumb,
.algo-modal .algo-list::-webkit-scrollbar-thumb,
.lexicon-modal .lexicon-list::-webkit-scrollbar-thumb,
.api-card-preview::-webkit-scrollbar-thumb,
.params-pre::-webkit-scrollbar-thumb,
.mapping-table :deep(.ant-table-body)::-webkit-scrollbar-thumb {
    background: #c7d1e6;
    border-radius: 8px;
}

.index-modal .im-list::-webkit-scrollbar-thumb:hover,
.algo-modal .algo-list::-webkit-scrollbar-thumb:hover,
.lexicon-modal .lexicon-list::-webkit-scrollbar-thumb:hover,
.api-card-preview::-webkit-scrollbar-thumb:hover,
.params-pre::-webkit-scrollbar-thumb:hover,
.mapping-table :deep(.ant-table-body)::-webkit-scrollbar-thumb:hover {
    background: #aebce0;
}

.page-header {
    height: 40px;
    display: flex;
    align-items: center;
    border-bottom: 1px solid #eaeaea;
    margin-bottom: 12px;
    margin-left: 24px;
}

.page-header .back {
    color: #1677ff;
    cursor: pointer;
    font-size: 16px;
}

.page-header .sep {
    margin: 0 12px;
    color: #999;
}

.page-header .title {
    font-weight: 500;
    font-size: 16px;
}

.form {
    width: 100%;
    max-width: 1200px;
}

.form :deep(.ant-form-item) {
    margin-bottom: 10px;
}

.form .row-40 :deep(.ant-form-item-control) {
    line-height: 40px;
}

.form :deep(.ant-input),
.form :deep(.ant-select-selector),
.form :deep(.ant-input-number),
.form :deep(.ant-btn) {
    height: 32px;
}

.vector-card {
    border: 1px solid #d9d9d9;
    padding: 12px 12px 4px;
    margin: 8px 0 16px 140px;
    border-radius: 2px;
    width: calc(100% - 140px - 24px);
}

.vector-card .inner :deep(.ant-form-item-label) {
    width: 120px;
}

.vector-card .inner :deep(.ant-form-item-control) {
    line-height: 40px;
}

.api-card {
    width: calc(100% - 24px);
    border: 1px solid #d9d9d9;
    border-radius: 2px;
    background: #fff;
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.api-card-preview {
    overflow: hidden;
}

.api-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: #f6f7fb;
    border-radius: 2px;
    padding: 10px 12px;
}

.api-text {
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    font-size: 13px;
    white-space: normal;
    word-break: break-all;
    overflow: visible;
}

.api-ops :deep(.ant-btn) {
    padding: 0 6px;
}

.index-table-card {
    margin-left: 140px;
    width: calc(100% - 140px - 24px);
    border: 1px solid #d9d9d9;
    border-radius: 2px;
    background: #fff;
    overflow: hidden;
    margin-bottom: 12px;
}

.index-table-card .index-table-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 6px 10px;
    background: #eef3ff;
}

.index-table-card .index-table-head .name {
    font-weight: 600;
}

.index-table-card .index-table-head .close {
    cursor: pointer;
    color: #666;
}

.index-table-card .mapping-table :deep(.ant-table-thead > tr > th) {
    background-color: #F4F8FD !important;
    font-weight: 500;
}

.del-btn {
    padding: 0 6px;
    color: #ff4d4f;
}

.multi-wrap :deep(.ant-select-selector) {
    min-height: 32px;
    height: auto !important;
    line-height: normal !important;
    white-space: normal;
    align-items: flex-start;
    padding-top: 2px;
    padding-bottom: 2px;
}

.multi-wrap :deep(.ant-select-selection-overflow) {
    flex-wrap: wrap;
}

.multi-wrap :deep(.ant-select-selection-item) {
    height: 24px;
    line-height: 24px;
}

.mapping-table :deep(.ant-table-tbody > tr > td) {
    padding: 6px 10px;
    vertical-align: top;
}

.cell-stretch {
    width: 100%;
}

.cell-stretch :deep(.ant-input),
.cell-stretch :deep(.ant-select) {
    width: 100%;
}

:deep(.ant-select-dropdown) {
    max-width: 560px;
}

.empty-select {
    min-height: 32px;
    padding: 0 8px;
    border: 1px dashed #d9d9d9;
    background: #fafafa;
    border-radius: 2px;
    color: #999;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.empty-select .tip {
    font-size: 13px;
}

.index-error-tip {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background: #fff1f0;
    border-top: 1px solid #ffe1df;
    color: #d9363e;
    font-size: 13px;
}

.index-error-tip .dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #d9363e;
}

.index-add-row {
    border-top: 1px solid #eee;
}

.index-add-row .add-btn {
    text-align: center;
    background: #e6efff;
    color: #3b6cff;
    padding: 10px 0;
    cursor: pointer;
    user-select: none;
}

.index-modal :deep(.ant-modal-content),
.algo-modal :deep(.ant-modal-content),
.lexicon-modal :deep(.ant-modal-content) {
    padding: 10px 0 8px;
}

.index-modal .im-head,
.algo-modal .im-head,
.lexicon-modal .im-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 16px 10px;
    border-bottom: 1px solid #eee;
    margin-bottom: 8px;
}

.index-modal .im-head .im-title,
.algo-modal .im-head .im-title,
.lexicon-modal .im-head .im-title {
    font-weight: 600;
}

.index-modal .im-footer,
.algo-modal .im-footer,
.lexicon-modal .im-footer {
    display: flex;
    justify-content: center;
    padding: 12px 0 6px;
}

.index-modal .im-list {
    display: grid;
    grid-template-columns: 1fr;
    row-gap: 8px;
    padding: 0 16px;
    max-height: 360px;
    overflow: auto;
}

.index-modal .im-list :deep(.ant-radio-wrapper) {
    display: flex;
    align-items: center;
}

.algo-modal .im-head .im-title,
.lexicon-modal .im-head .im-title {
    font-weight: 600;
}

.algo-modal .algo-list、 .lexicon-modal .lexicon-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 8px 16px 0 16px;
    max-height: 360px;
    overflow: auto;
}

.algo-modal .algo-list :deep(.ant-checkbox-wrapper),
.lexicon-modal .lexicon-list :deep(.ant-checkbox-wrapper) {
    margin: 0;
    display: flex;
    align-items: center;
    line-height: 24px;
}

.algo-picked-wrap {
    margin-left: 140px;
    width: calc(100% - 140px - 24px);
    padding: 8px 0 4px;
}

.algo-picked-row {
    display: grid;
    grid-template-columns: max-content 1fr max-content;
    align-items: center;
    gap: 8px;
    margin-bottom: 10px;
}

.algo-picked-row :deep(.ant-select) {
    width: 100%;
}

.algo-name {
    color: #333;
}

.lexicon-chips {
    margin-left: 140px;
    width: calc(100% - 140px - 24px);
    border: 1px solid #d9d9d9;
    padding: 10px 12px;
    border-radius: 2px;
    background: #fff;
    margin-bottom: 12px;
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    background: #e6ecff;
    color: #3b6cff;
    padding: 6px 10px;
    border-radius: 4px;
    margin-right: 12px;
    margin-bottom: 8px;
    font-size: 14px;
}

.chip .x {
    font-style: normal;
    margin-left: 4px;
    cursor: pointer;
    user-select: none;
    line-height: 1;
}

.chip .x:hover {
    color: #ff4d4f;
}

:deep(.ant-table-thead > tr > th) {
    background-color: #F4F8FD !important;
}

.page-actions {
    width: 100%;
    display: flex;
    justify-content: center;
    gap: 12px;
    margin: 18px 0 8px;
}

.modal-narrow :deep(.ant-modal) {
    max-width: 92vw;
}

@media (max-width: 900px) {

    .index-table-card,
    .algo-picked-wrap,
    .lexicon-chips,
    .vector-card,
    .api-card {
        margin-left: 0;
        width: 100%;
    }
}

.mapping-table :deep(.ant-table-tbody > tr > td:nth-child(3)) {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
}

.api-row-block {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 10px 12px;
    border: 1px solid #eef0f4;
    border-radius: 6px;
    background: #fff;
}

.api-row-block+.api-row-block {
    margin-top: 12px;
}

.api-type-line {
    display: flex;
    align-items: center;
    gap: 6px;
}

.api-type-line .label,
.api-params .label {
    color: #666;
    font-size: 13px;
}

.api-params .params-pre {
    margin: 6px 0 0 0;
    padding: 10px 12px;
    background: #f8f9fb;
    border: 1px solid #eef0f4;
    border-radius: 4px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    font-size: 13px;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-all;
}
</style>
